package peer;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import error.AuthenticationException;
import error.DgoogleException;
import error.NetworkException;
import error.BootServerException;
import file.LocalSharedFile;
import file.RemoteSharedFile;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A simple line based protocol to talk with bootstrap server designed by ???
 * 
 * @note This class is not thread safe by the moment.  Will add it later.
 * @author Iris
 *
 */
class SimpleBootProtocol implements IBootProtocol {

    String name;
    String passwd;
    String hashed_passwd;
    String ip;
    String email;
    short port;
    boolean logined = false;
    boolean registered=false;
    Socket sock = null;
    MessageDigest digest;
    String digested_pwd;
    PrintWriter output;
    BufferedReader input;
    private String local_ip;
    String human_user_name;

    public SimpleBootProtocol(String name, String human_user_name, String passwd,
            String ip, short port, String email) {
        this.name = name;
        this.passwd = passwd;
        this.ip = ip;
        this.port = port;
        this.email = email;
        this.human_user_name = human_user_name;

        this.digested_pwd = calculate_digested_pwd(this.passwd);
    }

    private String calculate_digested_pwd(String pwd)
    {
        String dp = "";
        try {
            this.digest = MessageDigest.getInstance("sha-1");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }

        this.digest.update(pwd.getBytes());

        byte[] hash = this.digest.digest();


        // Convert hash to string so it could be used in a text based protocol.
        for(byte b : hash)
        {
            dp += String.format("%x", b);
        }

        return dp;
    }

    public boolean is_logined() {
        return this.logined;
    }

    public void login() throws DgoogleException {
        if(logined)
            return;

        //Only create socket once.
        reconnect();

        local_ip = sock.getLocalAddress().getHostAddress();
        output.printf("L|%s|%s|%s|\n", name, digested_pwd, local_ip);
        output.flush();

        String res;
        try {
            res = input.readLine();
        } catch (IOException e) {
            this.close();
            throw new NetworkException(e);
        }

        String[] paras = res.split("\\|");
        // Check login result.
        if(paras.length == 3 && paras[0].compareTo("L") == 0 && paras[1].compareTo(name) == 0)
        {
            this.logined = true;
        }
        else
        {
            throw new AuthenticationException("Login failed.");
        }

    }

    /**
     * Create TCP connection to server.  If connection is already there, close it and re-connect.
     * @throws NetworkException
     */
    private void connect() throws NetworkException {
        if(null != sock)
        {
			try {
				sock.close();				
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				sock = null;
			}
        }
        
        try {
            sock = new Socket(ip, port);
            output = new PrintWriter(sock.getOutputStream( ), true);
            input = new BufferedReader(new InputStreamReader(sock.getInputStream( )));
        } catch (UnknownHostException e) {
            this.close();
            throw new NetworkException(e);
        } catch (IOException e) {
            this.close();
            throw new NetworkException(e);
        }     
    }


    public void register() throws DgoogleException {
        if(logined)
            return;

        if(registered)
            return;

        connect();

        // Here name and user name should be different.  
        // But we d'not enough time for this kind of small functions.
        output.printf("R|%s|%s|%s|%s|\n", human_user_name, name, this.email, this.digested_pwd);

        //No need to flush
        //output.flush();

        String res;
        try {
            res = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }

        //Check Result
        String[] paras = res.split("\\|");
        if(paras.length != 2 || paras[1].compareTo("Done") == 0)
        {
            this.registered = true;
        }
        else
        {
            String msg = "Register failed.";
            if(paras.length ==2) //Check if sammy give us error info.
            {
                msg = paras[1];
            }
            throw new AuthenticationException(msg);
        }
    }


    // Do not throw any exception here but this is just close.
    @Override
    public void close() {
        try {
            // Only send 'O' message when user login.
            if(logined)
            {
                reconnect();
                output.printf("O|%s|%s|%s|\n",
                        name,
                        sock.getLocalAddress().getHostAddress(),
                        util.DateTime.DateToString());
            }
            if(sock != null)
                sock.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NetworkException ex) {
            Logger.getLogger(SimpleBootProtocol.class.getName()).log(Level.SEVERE, null, ex);
            return;
        }
        sock = null;
    }

    public boolean is_registered() {
        return this.registered;
    }

    public void resetPassword() throws DgoogleException{

        reconnect();

        output.printf("S|%s|\n", name);

        //No need to flush
        //output.flush();

        String res;
        try {
            res = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }

        //Check Result
        String[] paras = res.split("\\|");
        if(paras.length == 2 && (paras[1].compareTo(name) == 0))
        {
            return;
        }
        else
        {
            throw new AuthenticationException("Reset passowrd failed.");
        }	
    }
    public void error(String type, String details) throws DgoogleException{

        reconnect();

        output.printf("!|%s|%s|\n", type, details);

        String res;
        try {
            res = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }

        output.printf("Response %s", res);
    }
    public void changePassword( 
            String new_pass) throws DgoogleException{

        reconnect();

        String new_digested_pwd = calculate_digested_pwd(new_pass);

        output.printf("C|%s|%s|%s|\n", this.name, this.digested_pwd, new_digested_pwd);

        String res;
        try {
            res = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }

        //Check Result
        String[] paras = res.split("\\|");
        if(paras.length == 3 && paras[2].compareTo(new_digested_pwd) == 0)
        {
            this.passwd = new_pass;
            this.digested_pwd = new_digested_pwd;
        }
        else
        {
            throw new AuthenticationException("Change passowrd failed.");
        }	
    }
    /**Client: P|Username\n
Server: T|Send\n
Client: T|filename|digest|hash|username|ip|filesize|time|\n
Server: T|Done|\n 
**/

    public LocalSharedFile publish(String path) throws DgoogleException, IOException {
        if(!logined)
            throw new AuthenticationException("Not login yet");
        
        reconnect();
        
        LocalSharedFile file = new LocalSharedFile(path);
        
        output.printf("P|%s|\n",name);
        String res;
        
        try {
            res = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }
        //Check Result
        String[] paras = res.split("\\|");
        if(paras.length == 2 && paras[0].compareTo("T") == 0)
        {
            //Nothing to do.
        }
        else
        {
            throw new AuthenticationException("Publish failed.");
        }
      
        
        output.printf("T|%s|%s|%s|%s|%s|%d|%s|\n",
                file.name(),
                file.abstraction(),
                file.signature(),
                name,
                local_ip,
                file.length(),
                file.lastModified());
        /////////////////////////////
        //output.printf("T|%s|1|%s|%s|%s|%d|%s\n",
        //        name,
         //       URLEncoder.encode(file.name(), "utf8"),
         //       file.signature(),
         //       URLEncoder.encode(file.abstraction(), "utf8"),
         //       file.length(),
         //       URLEncoder.encode(file.lastModified(), "utf8"));
        String res2;
        try {
            res2 = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }
        //Check Result
        String[] paras2 = res2.split("\\|");
        if(paras2.length == 2 || paras2[0].compareTo("T") == 0)
        {
            output.printf("P|Done|\n");
        }
        else
        {
            throw new AuthenticationException("Publish failed.");
        }

        //sammy send a T!Done and a P!Done.  So we need to read one more line.
        input.readLine();
        
        return file;
    } //end of publish

    /**
     * Sammy close TCP connection after login.  We have no choice but connect again.
     * 
     * @throws NetworkException
     */
	private void reconnect() throws NetworkException {
		try {
                    if(null != input)
                    {
			int n = input.read();
			if(n == -1)
                            connect();
                    }
                    else
                    {
                        connect();
                    }

		} catch (IOException e1) {
			this.connect();
	    }		
	}

	/**
	 *  Query file from server
	 */
    public List<RemoteSharedFile> query(String keyWord) throws DgoogleException {
        if(!logined)
            throw new AuthenticationException("Not login yet");
        
        reconnect();

        output.printf("Q|%s|%d|%s|\n", 
                name,0,
                keyWord);
        String res;
        try {
            res = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }

        //Check Result
        String[] paras = res.split("\\||~");
        if(paras.length < 3 || 0 == paras[0].compareTo("!"))
        {
            String msg = "Query failed.";
            if(paras.length == 2)
            {
                msg = "No files found.";
            }

            throw new BootServerException(msg);
        }

        int total_file_num = Integer.parseInt(paras[2]);
        List<RemoteSharedFile> result = parseQueryResult(paras, keyWord);
        while(result.size() < total_file_num)
        {
            result.addAll(query_more(result.size(), keyWord));
        }

        //Send an empty line to tell sammy we are done.
        output.println("N");
        
        //Sort list according to how many times a keyword appears in the digest.
        Collections.sort(result);
        
        return result;
    }

    private List<RemoteSharedFile> parseQueryResult(String[] paras, String keyWord)
        throws DgoogleException {
            int file_num = Integer.parseInt(paras[2]);

            List<RemoteSharedFile> files = new ArrayList<RemoteSharedFile>();

            if(file_num == 0)
                return files;
            else 
            {
                int i = 0;
                for(; 5 + i < paras.length; i +=3 )
                {
                    String q_name = paras[3 + i];
                    String q_digest = paras[4 + i];
                    String q_ip = paras[5 + i];
                    
                    RemoteSharedFile f = new RemoteSharedFile(q_name, q_digest, q_ip, keyWord);
                    files.add(f);
                }
            }

            return files;
        }

    private List<RemoteSharedFile> query_more(int seq, String kewWord) throws DgoogleException {
        if(!logined)
            throw new AuthenticationException("Not login yet");

        output.printf("M|%s|0|%d|\n", 
                name,
                seq);

        String res;
        try {
            res = input.readLine();
        } catch (IOException e) {
            throw new NetworkException(e);
        }

        //Check Result
        String[] paras = res.split("\\|");
        if(paras.length < 3)
        {
            throw new AuthenticationException("Query failed.");
        }

        return parseQueryResult(paras, kewWord);
    }
}

